/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */
package com.iwindplus.gateway.server.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.iwindplus.base.domain.constant.CommonConstant.HeaderConstant;
import com.iwindplus.base.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.base.domain.enums.BizCodeEnum;
import com.iwindplus.base.domain.exception.BizException;
import com.iwindplus.base.domain.vo.UserBaseVO;
import com.iwindplus.base.util.CryptoUtil;
import com.iwindplus.base.util.HttpsUtil;
import com.iwindplus.base.util.JacksonUtil;
import com.iwindplus.base.util.ReactorUtil;
import com.iwindplus.base.util.UrlUtil;
import com.iwindplus.base.web.domain.property.FilterProperty;
import com.iwindplus.gateway.server.client.AuthClient;
import com.iwindplus.gateway.server.client.MgtClient;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.FilterConstant;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.ServerWebExchangeContextConstant;
import com.iwindplus.gateway.server.domain.property.GatewayProperty;
import com.iwindplus.gateway.server.domain.property.GatewayProperty.AuthConfig;
import com.iwindplus.gateway.server.domain.property.GatewayProperty.LogConfig;
import com.iwindplus.gateway.server.util.GatewayUtil;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

/**
 * 认证过滤器（完全非阻塞实现）
 *
 * @author zengdegui
 * @since 2020/4/15
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private final GatewayProperty property;
    private final FilterProperty filterProperty;
    private final AuthClient authClient;
    private final MgtClient mgtClient;
    private final Map<String, Map<String, Boolean>> permissionCache = new ConcurrentHashMap<>(16);

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public AuthFilter(GatewayProperty property, FilterProperty filterProperty, AuthClient authClient, MgtClient mgtClient) {
        this.property = property;
        this.filterProperty = filterProperty;
        this.authClient = authClient;
        this.mgtClient = mgtClient;
    }

    @Override
    public int getOrder() {
        return FilterConstant.FILTER_AUTH_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 开关未启用 or 白名单直接放行
        if (skip(exchange)) {
            return chain.filter(exchange);
        }

        final long start = System.nanoTime();

        return checkAccessToken(exchange)
            .flatMap(tuple -> {
                ServerWebExchange newExchange = tuple.getT1();
                UserBaseVO user = tuple.getT2();

                ReactorUtil.setAttribute(newExchange, ServerWebExchangeContextConstant.USER_INFO, user);

                return checkApiPermission(newExchange, user)
                    .then(chain.filter(newExchange))
                    .doFinally(signal -> log.info("{} execute cost={} ms", this.getClass().getSimpleName(),
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
            });
    }


    private boolean skip(ServerWebExchange exchange) {
        final AuthConfig cfg = this.property.getAuth();
        if (!cfg.getEnabled()) {
            return true;
        }
        final Boolean whited = ReactorUtil.getAttribute(exchange, ServerWebExchangeContextConstant.WHITED_FLAG);
        return Boolean.TRUE.equals(whited);
    }

    private Mono<Tuple2<ServerWebExchange, UserBaseVO>> checkAccessToken(ServerWebExchange exchange) {
        final AuthConfig authConfig = property.getAuth();
        final LogConfig logConfig = property.getLog();
        return Mono.justOrEmpty(HttpsUtil.getAuthorization(exchange.getRequest()))
            .switchIfEmpty(GatewayUtil.asyncPublishErrorLog(exchange, logConfig, BizCodeEnum.TOKEN_NOT_EXIST.getBizStatus())
                .then(Mono.error(new BizException(BizCodeEnum.TOKEN_NOT_EXIST))))
            .map(auth -> CharSequenceUtil.replace(auth, HeaderConstant.BEARER_TYPE, SymbolConstant.EMPTY_STR).trim())
            .flatMap(token -> authConfig.getEnabledRemoteToken()
                ? authClient.checkAccessToken(token)
                : Mono.justOrEmpty(HttpsUtil.getUserInfo(token)))
            .switchIfEmpty(Mono.defer(() -> {
                if (!authConfig.getIgnoredLogApi().contains(exchange.getRequest().getPath().value())) {
                    return GatewayUtil.asyncPublishErrorLog(exchange, logConfig, BizCodeEnum.INVALID_ACCESS_TOKEN.getBizStatus())
                        .then(Mono.error(new BizException(BizCodeEnum.INVALID_ACCESS_TOKEN)));
                }
                return Mono.error(new BizException(BizCodeEnum.INVALID_ACCESS_TOKEN));
            })).map(user -> {
                String jsonStr = JacksonUtil.toJsonStr(user);
                String userInfoStr = CryptoUtil.encrypt(jsonStr, filterProperty.getCrypto());

                // 写 request header
                ServerHttpRequest newReq = exchange.getRequest()
                    .mutate()
                    .header(HeaderConstant.X_USER_INFO, userInfoStr)
                    .build();
                exchange.mutate().request(newReq);

                ServerWebExchange newExchange = exchange.mutate().request(newReq).build();
                return Tuples.of(newExchange, user);
            });
    }

    private Mono<Void> checkApiPermission(ServerWebExchange exchange, UserBaseVO user) {
        String path = GatewayUtil.getRelativePath(exchange);

        if (Boolean.FALSE.equals(property.getAuth().getEnabledApiPermission())
            || hasIgnoredWhitedFlag(path)) {
            return Mono.empty();
        }

        final Long orgId = user.getOrgId();
        final Long userId = user.getUserId();

        // 通用权限API
        final List<String> generalApi = property.getAuth().getGeneralApi();
        if (CollUtil.isNotEmpty(generalApi) && UrlUtil.isMatch(generalApi, path)) {
            return Mono.empty();
        }

        // 1. 缓存命中
        String key = this.buildPermissionKey(orgId, userId);
        Map<String, Boolean> userPerms = permissionCache.get(key);
        Boolean cached = userPerms == null ? null : userPerms.get(path);
        if (cached != null) {
            return cached ? Mono.empty() : buildErrorInfo(exchange);
        }

        // 2. 缓存未命中 → 远程 + 写缓存
        return mgtClient.checkApiByUserId(orgId, userId, path)
            .defaultIfEmpty(false)
            .doOnNext(granted ->
                permissionCache.computeIfAbsent(key, k -> new ConcurrentHashMap<>(16))
                    .put(path, granted)
            )
            .flatMap(granted -> granted ? Mono.empty() : buildErrorInfo(exchange));
    }

    private Mono<Void> buildErrorInfo(ServerWebExchange exchange) {
        return GatewayUtil.asyncPublishErrorLog(exchange, property.getLog(), BizCodeEnum.NO_API_PERMISSION.getBizStatus())
            .then(Mono.error(new BizException(BizCodeEnum.NO_API_PERMISSION)));
    }

    private boolean hasIgnoredWhitedFlag(String requestPath) {
        List<String> ignoredPatterns = property.getAuth().getIgnoredApi();
        if (CollUtil.isNotEmpty(ignoredPatterns)) {
            List<String> whiteList = ignoredPatterns.stream().filter(Objects::nonNull).distinct().toList();
            return UrlUtil.isMatch(whiteList, requestPath);
        }
        return false;
    }

    private String buildPermissionKey(Long orgId, Long userId) {
        return String.format("%s_%s", orgId, userId);
    }
}